﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Chefbook.DataObjects;
using Chefbook.DataAccess;
using Chefbook.HelperMethods;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Chefbook.UserActions;
using System.Windows;
using Reversible;
using Reversible.Extensions;

namespace Chefbook.ViewModels
{
    /// <summary>
    /// A UI-friendly wrapper for a Recipe
    /// </summary>
    public class RecipeViewModel : ViewModelBase
    {
        private Recipe recipeModel;
        private ChefbookXmlRepository repository;
        private ObservableCollection<IngredientViewModel> ingredients = new ObservableCollection<IngredientViewModel>();
        private ObservableCollection<CategoryViewModel> categories = new ObservableCollection<CategoryViewModel>();
        private ObservableCollection<string> categorypaths = new ObservableCollection<string>();
        private UndoRedoSession undoSession;

        /// <summary>
        /// Creates a new UI-friendly wrapper for a Recipe
        /// </summary>
        /// <param name="recipe">The Recipe to be wrapped</param>
        /// <param name="repo">Repository for recipes and categories</param>
        public RecipeViewModel(Recipe recipe, ChefbookXmlRepository repo)
        {
            repository = repo;
            this.RecipeModel = recipe;
            //We initialize the UndoRedoSession in the setter for RecipeModel because we don't want anything 
            //done before that to be available in the undo/redo stack since they are part of initialization
        }

        /// <summary>
        /// Creates a new UI-friendly wrapper for a Recipe
        /// </summary>
        /// <param name="recipe">The Recipe to be wrapped</param>
        public RecipeViewModel(Recipe recipe) : this(recipe, ChefbookXmlRepository.Instance) { }

        /// <summary>
        /// Creates a new UI-friendly wrapper for a Recipe to be specified later
        /// </summary>
        /// <param name="repo">Repository for recipes and categories</param>
        public RecipeViewModel(ChefbookXmlRepository repo) : this(null, repo) { }

        /// <summary>
        /// Creates a new UI-friendly wrapper for a Recipe to be specified later
        /// </summary>
        public RecipeViewModel() : this(null, ChefbookXmlRepository.Instance) { }

        /// <summary>
        /// Gets or sets the wrapped Recipe.  Can only be set once.
        /// </summary>
        public Recipe RecipeModel
        {
            get { return recipeModel; }
            set
            {
                if (value == null) return;
                if (recipeModel != null)
                    throw new InvalidOperationException("RecipeModel can only be set once");

                recipeModel = value;
                ((INotifyCollectionChanged)recipeModel.Categories).CollectionChanged += new NotifyCollectionChangedEventHandler(BaseRecipeCategoriesCollectionChanged);
                ((INotifyCollectionChanged)recipeModel.Ingredients).CollectionChanged += new NotifyCollectionChangedEventHandler(BaseRecipeIngredientCollectionChanged);
                foreach (var ing in recipeModel.Ingredients)
                {
                    AddIngredient(new IngredientViewModel(ing));
                }
                foreach (var cat in recipeModel.Categories)
                {
                    addCategorySafely(cat);
                }
                undoSession = new UndoRedoSession();
            }
        }

        /// <summary>
        /// Gets or sets the name of this recipe
        /// </summary>
        public string Name
        {
            get { return RecipeModel.Name; }
            set
            {
                if (value == RecipeModel.Name) return;
                using (Transaction trans = undoSession.Begin(
                    "Change recipe name from {0} to {1}".FormatWith(RecipeModel.Name, value)))
                {
                    Transaction.AddPropertyChange(v =>
                    {
                        RecipeModel.Name = v;
                        OnPropertyChanged("Name");
                    }, RecipeModel.Name, value);
                    RecipeModel.Name = value;
                    OnPropertyChanged("Name");
                    trans.Commit();
                }
                
            }
        }

        /// <summary>
        /// Gets or sets the description of this recipe
        /// </summary>
        public string Description
        {
            get { return RecipeModel.Description; }
            set
            {
                if (value == RecipeModel.Description) return;
                using (Transaction trans = undoSession.Begin(
                    "Change recipe description from {0} to {1}".FormatWith(RecipeModel.Description, value)))
                {
                    Transaction.AddPropertyChange(v =>
                    {
                        RecipeModel.Description = v;
                        OnPropertyChanged("Description");
                    }, RecipeModel.Description, value);
                    RecipeModel.Description = value;
                    OnPropertyChanged("Description");
                    trans.Commit();
                }   
            }
        }

        /// <summary>
        /// Gets or sets the directions for this recipe
        /// </summary>
        public string Directions
        {
            get { return RecipeModel.Directions; }
            set
            {
                if (value == RecipeModel.Directions) return;
                using (Transaction trans = undoSession.Begin(
                    "Change recipe directions from {0} to {1}".FormatWith(RecipeModel.Directions, value)))
                {
                    Transaction.AddPropertyChange(v =>
                        {
                            RecipeModel.Directions = v;
                            OnPropertyChanged("Directions");
                        }, RecipeModel.Directions, value);
                    RecipeModel.Directions = value;
                    OnPropertyChanged("Directions");
                    trans.Commit();
                }
            }
        }

        /// <summary>
        /// Gets or sets the minutes portion of the cook time
        /// </summary>
        public int CookTimeMinutes
        {
            get { return RecipeModel.CookTime.Minutes; }
            set
            {
                TimeSpan minutesPart = TimeSpan.FromMinutes(value);
                TimeSpan hoursPart = TimeSpan.FromHours(RecipeModel.CookTime.Hours);

                bool minutesChanged = minutesPart.Minutes != RecipeModel.CookTime.Minutes;
                bool hoursChanged = minutesPart.Hours > 0;
                if (!(minutesChanged || hoursChanged))
                {return;}
                var newTimespan = hoursPart.Add(minutesPart);
                using (Transaction trans = undoSession.Begin(
                    "Change recipe cook time from {0}:{1} to {2}:{3}".FormatWith(
                    RecipeModel.CookTime.Hours,RecipeModel.CookTime.Minutes,
                    newTimespan.Hours, newTimespan.Minutes)))
                {
                    Transaction.AddPropertyChange(v =>
                    {
                        RecipeModel.CookTime = v;
                        if (minutesChanged)
                        { base.OnPropertyChanged("CookTimeMinutes"); }
                        if (hoursChanged)
                        { base.OnPropertyChanged("CookTimeHours"); }
                    }, RecipeModel.CookTime, newTimespan);
                    RecipeModel.CookTime = newTimespan;
                    if (minutesChanged)
                    { base.OnPropertyChanged("CookTimeMinutes"); }
                    if (hoursChanged)
                    { base.OnPropertyChanged("CookTimeHours"); }
                    trans.Commit();
                }
            }
        }

        /// <summary>
        /// Gets or sets the hours portion of the cook time
        /// </summary>
        public int CookTimeHours
        {
            get { return RecipeModel.CookTime.Hours; }
            set
            {
                if (value == RecipeModel.CookTime.Hours) return;

                TimeSpan minutesPart = TimeSpan.FromMinutes(RecipeModel.CookTime.Minutes);
                using (Transaction trans = undoSession.Begin(
                    "Change recipe cook time from {0}:{1} to {2}:{1}".FormatWith(
                    RecipeModel.CookTime.Hours, RecipeModel.CookTime.Minutes,
                    value)))
                {
                    Transaction.AddPropertyChange(v =>
                    {
                        RecipeModel.CookTime = minutesPart.Add(TimeSpan.FromHours(v));
                        OnPropertyChanged("CookTimeHours");
                    }, CookTimeHours, value);
                    RecipeModel.CookTime = minutesPart.Add(TimeSpan.FromHours(value));
                    OnPropertyChanged("CookTimeHours");
                    trans.Commit();
                }
            }
        }

        /// <summary>
        /// Gets or sets the minutes portion of the preparation time
        /// </summary>
        public int PrepTimeMinutes
        {
            get { return RecipeModel.PrepTime.Minutes; }
            set
            {
                TimeSpan minutesPart = TimeSpan.FromMinutes(value);
                TimeSpan hoursPart = TimeSpan.FromHours(RecipeModel.PrepTime.Hours);
                bool minutesChanged = minutesPart.Minutes != RecipeModel.PrepTime.Minutes;
                bool hoursChanged = minutesPart.Hours > 0;
                if (!(minutesChanged || hoursChanged))
                { return; }
                var newTimespan = hoursPart.Add(minutesPart);
                using (Transaction trans = undoSession.Begin(
                    "Change recipe prep time from {0}:{1} to {2}:{3}".FormatWith(
                    RecipeModel.PrepTime.Hours, RecipeModel.PrepTime.Minutes,
                    newTimespan.Hours, newTimespan.Minutes)))
                {
                    Transaction.AddPropertyChange(v =>
                    {
                        RecipeModel.PrepTime = v;
                        if (minutesChanged)
                        { base.OnPropertyChanged("PrepTimeMinutes"); }
                        if (hoursChanged)
                        { base.OnPropertyChanged("PrepTimeHours"); }
                    }, RecipeModel.PrepTime, newTimespan);
                    RecipeModel.PrepTime = newTimespan;
                    if (minutesChanged)
                    { base.OnPropertyChanged("PrepTimeMinutes"); }
                    if (hoursChanged)
                    { base.OnPropertyChanged("PrepTimeHours"); }
                    trans.Commit();
                }
            }
        }

        /// <summary>
        /// Gets or sets the hours portion of the preparation time
        /// </summary>
        public int PrepTimeHours
        {
            get { return RecipeModel.PrepTime.Hours; }
            set
            {
                if (value == RecipeModel.PrepTime.Hours) return;
                using (Transaction trans = undoSession.Begin(
                    "Change recipe cook time from {0}:{1} to {2}:{1}".FormatWith(
                    RecipeModel.PrepTime.Hours, RecipeModel.PrepTime.Minutes,
                    value)))
                {
                    TimeSpan minutesPart = TimeSpan.FromMinutes(RecipeModel.PrepTime.Minutes);
                    Transaction.AddPropertyChange(v =>
                        {
                            TimeSpan minutesPartInner = TimeSpan.FromMinutes(RecipeModel.PrepTime.Minutes);
                            RecipeModel.PrepTime = minutesPartInner.Add(TimeSpan.FromHours(v));
                            OnPropertyChanged("PrepTimeHours");
                        }, PrepTimeHours, value);
                    RecipeModel.PrepTime = minutesPart.Add(TimeSpan.FromHours(value));
                    OnPropertyChanged("PrepTimeHours");
                    trans.Commit();
                }
            }
        }


        /// <summary>
        /// Gets a read-only collection of the ingredients in this recipe
        /// </summary>
        public ReadOnlyObservableCollection<IngredientViewModel> Ingredients
        {
            get { return new ReadOnlyObservableCollection<IngredientViewModel>(ingredients); }
        }

        /// <summary>
        /// Gets all the full paths of the recipe's categories
        /// </summary>
        public ReadOnlyObservableCollection<string> CategoryPaths
        {
            get { return new ReadOnlyObservableCollection<string>(categorypaths); }
        }
        ICommand addIngredientCommand;
        public ICommand AddIngredientCommand
        {
            get
            {
                if (addIngredientCommand == null)
                {
                    addIngredientCommand = new CustomCommand(
                        param => this.AddIngredient(new IngredientViewModel(new Ingredient())));
                }
                return addIngredientCommand;
            }
        }
        /// <summary>
        /// Adds an ingredient to this recipe
        /// </summary>
        /// <param name="ingredient">The ingredient to add</param>
        public void AddIngredient(IngredientViewModel ingredient)
        {
            if (undoSession == null)
            {
                AddIngredientOutsideTransaction(ingredient);
                return;
            }
            using (Transaction trans = undoSession.Begin("Add ingredient: {0}".FormatWith(ingredient.Name)))
            {
                Transaction.AddPropertyChange(v => v(),  //Passing in a delegate to the delegate, lol
                    new Action(() => RemoveIngredientOutsideTransaction(ingredient)), //Undo
                    new Action(() => AddIngredientOutsideTransaction(ingredient)));   //Redo
                AddIngredientOutsideTransaction(ingredient);
                trans.Commit();
            }
        }

        private void AddIngredientOutsideTransaction(IngredientViewModel ingredient)
        {
            ingredient.IngredientDeleted += ingredientDeletedEventHandler;
            ingredients.AddIfNewAndNotNull(ingredient);
            RecipeModel.AddIngredient(ingredient.IngredientModel);
        }

        /// <summary>
        /// Removes an ingredient from this recipe
        /// </summary>
        /// <param name="ingredient">The ingredient to remove</param>
        public void RemoveIngredient(IngredientViewModel ingredient)
        {
            if (undoSession == null)
            {
                RemoveIngredientOutsideTransaction(ingredient);
                return;
            }
            using (Transaction trans = undoSession.Begin("Remove ingredient: {0}".FormatWith(ingredient.Name)))
            {
                Transaction.AddPropertyChange(v => v(),  //Passing in a delegate to the delegate, lol
                    new Action(() => AddIngredientOutsideTransaction(ingredient)), //Undo
                    new Action(() => RemoveIngredientOutsideTransaction(ingredient)));   //Redo
                RemoveIngredientOutsideTransaction(ingredient);
                trans.Commit();
            }
        }

        private void RemoveIngredientOutsideTransaction(IngredientViewModel ingredient)
        {
            ingredient.IngredientDeleted -= ingredientDeletedEventHandler;
            RecipeModel.RemoveIngredient(ingredient.IngredientModel);
        }

        /// <summary>
        /// Gets a read-only collection of the categories this recipe is in
        /// </summary>
        public ReadOnlyObservableCollection<CategoryViewModel> Categories
        {
            get { return new ReadOnlyObservableCollection<CategoryViewModel>(categories); }
        }

        /// <summary>
        /// Adds this recipe to a category
        /// </summary>
        /// <param name="category">The category to add to</param>
        public void AddToCategory(CategoryViewModel category)
        {
            if (undoSession == null)
            {
                RecipeModel.AddCategory(category.CategoryModel);
                return;
            }
            using (Transaction trans = undoSession.Begin("Add to category: {0}".FormatWith(category.Name)))
            {
                Transaction.AddPropertyChange(v => v(),  //Passing in a delegate to the delegate, lol
                    new Action(() => RecipeModel.RemoveCategory(category.CategoryModel)), //Undo
                    new Action(() => RecipeModel.AddCategory(category.CategoryModel)));   //Redo
                RecipeModel.AddCategory(category.CategoryModel);
                trans.Commit();
            }
        }

        /// <summary>
        /// Removes this recipe from a category
        /// </summary>
        /// <param name="category">The category to remove from</param>
        public void RemoveFromCategory(CategoryViewModel category)
        {
            if (undoSession == null)
            {
                RecipeModel.RemoveCategory(category.CategoryModel);
                return;
            }
            using (Transaction trans = undoSession.Begin("Add to category: {0}".FormatWith(category.Name)))
            {
                Transaction.AddPropertyChange(v => v(),  //Passing in a delegate to the delegate, lol
                    new Action(() => RecipeModel.AddCategory(category.CategoryModel)), //Undo
                    new Action(() => RecipeModel.RemoveCategory(category.CategoryModel)));   //Redo
                RecipeModel.RemoveCategory(category.CategoryModel);
                trans.Commit();
            }
        }

        /// <summary>
        /// Undoes the previous action
        /// </summary>
        /// <returns>true if an action ws undone, else false</returns>
        public bool Undo()
        {
            if (undoSession.CanUndo())
            {
                undoSession.Undo();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Redoes the previously undone action
        /// </summary>
        /// <returns>true if an action was redone, else false</returns>
        public bool Redo()
        {
            if (undoSession.CanRedo())
            {
                undoSession.Redo();
                return true;
            }
            return false;
        }
        /// <summary>
        /// Compares two RecipeViewModels for value equality
        /// </summary>
        /// <param name="recipeToCompare">The recipe to compare with</param>
        /// <returns>True if they represent the same value, else false</returns>
        public bool ValueEquals(RecipeViewModel recipeToCompare)
        {
            return RecipeModel.ValueEquals(recipeToCompare.RecipeModel);
        }

        /// <summary>
        /// Compares whether the parameter's wrapped recipe is equal to this wrapped recipe
        /// </summary>
        /// <param name="obj">The object to compare</param>
        /// <returns>True if it wraps the same Recipe, else false</returns>
        public override bool Equals(object obj)
        {
            var comparedRecipe = obj as RecipeViewModel;
            if (comparedRecipe == null)
            { return false; }
            return RecipeModel == comparedRecipe.RecipeModel;
        }

        /// <summary>
        /// Gets the hashcode of the wrapped Recipe
        /// </summary>
        /// <returns>The hashcode</returns>
        public override int GetHashCode()
        {
            return RecipeModel.GetHashCode();
        }

        /// <summary>
        /// Needed to safely add a category without causing an infinite loop
        /// </summary>
        /// <param name="category">The category to add</param>
        private void addCategorySafely(Category category)
        {
            CategoryViewModel catVM;
            if (repository.Categories.ContainsKey(category))
            {
                catVM = repository.Categories[category];

            }
            else
            {
                catVM = new CategoryViewModel(repository);
                repository.Categories[category] = catVM;
                catVM.CategoryModel = category;
            }
            categories.AddAndThrowIfNullOrNotNew(catVM);
            foreach (var item in categories)
            {
                var currentCategoryVM = item;
                do
                {
                    currentCategoryVM.PropertyChanged += itemPropertyChangedEventHandler;
                    currentCategoryVM = currentCategoryVM.Parent;
                } while (currentCategoryVM != null);
            }
            invalidateCategoryPaths();
        }

        void ingredientDeletedEventHandler(object sender, EventArgs e)
        {
            RemoveIngredient((IngredientViewModel)sender);
        }

        void itemPropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Parent" || e.PropertyName == "Name")
            {
                invalidateCategoryPaths();
            }
        }

        private void invalidateCategoryPaths()
        {
            List<string> newCategoryPaths = new List<string>();
            foreach (var item in categories)
            {

                StringBuilder builder = new StringBuilder();
                List<string> parentNames = new List<string>();
                var current = item;
                while (current != null)
                {
                    parentNames.Add(current.Name);
                    current = current.Parent;
                }
                for (int i = parentNames.Count - 1; i >= 0; i--)
                {
                    builder.Append(" > ");
                    builder.Append(parentNames[i]);
                }
                newCategoryPaths.Add(builder.ToString().Trim());
            }
            categorypaths.ModifyCollectionToMatchNewCollection(newCategoryPaths);
        }

        private void BaseRecipeCategoriesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    addCategorySafely((Category)e.NewItems[0]);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    categories.Remove(new CategoryViewModel(e.OldItems[0] as Category));
                    break;
                default:
                    throw new NotImplementedException();
            }
        }

        private void BaseRecipeIngredientCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    ingredients.AddIfNewAndNotNull(new IngredientViewModel(e.NewItems[0] as Ingredient));
                    break;
                case NotifyCollectionChangedAction.Remove:
                    ingredients.Remove(new IngredientViewModel(e.OldItems[0] as Ingredient));
                    break;
                default:
                    throw new NotImplementedException();
            }
        }

        internal RecipeViewModel MemberwiseClone()
        {
            return new RecipeViewModel(recipeModel.MemberwiseClone(), repository);
        }
    }
}